<template>
  <div class="img-upload-vessel" :style="{ '--file-width': `${width}px` }">
    <div ref="parent" class="complete-list">
      <div v-for="(item, index) in fileList" :key="item.__id" class="complete-item">
        <rest-hover-aciton
          :width="width"
          :height="height"
          :url="item.url"
          :poster="item.poster"
          :percentage="item.percentage"
          :uploadStatus="item.uploadStatus"
          :uploadErrorMsg="item.errMsg"
          @previewImage="() => previewImageClick(index)"
          @previewVideo="() => previewVideoClick(item.url, item.poster)"
          @delete="() => deleteClick(index)"
          @reUpload="reUploadClick(index)"
        ></rest-hover-aciton>
      </div>
      <div v-if="efficientNumb > 0" class="complete-item">
        <rest-select-file
          ref="restSelectFileRef"
          :width="newWidth"
          :height="newHeight"
          :hint="hint"
          :limit="efficientNumb"
          :accept="accept"
          :afterSelectFile="afterSelectFile"
          @change="uploadFileList"
        ></rest-select-file>
      </div>
      <div v-for="i in 10" :key="i" class="complete-item" style="height: 0; margin-top: 0"></div>
    </div>
    <!-- 预览图片 -->
    <rest-img-viewer ref="restImgViewerRef"></rest-img-viewer>
    <!-- 视频预览 -->
    <rest-video-viewer ref="restVideoViewerRef"></rest-video-viewer>
  </div>
</template>

<script setup lang="ts" generic="K extends string | string[] | Record<string, unknown> | Record<string, unknown>[]">
  import { getFirstFrameOfVideo, getImageWidthHeight } from '@/utils/file';
  import RestHoverAciton from '@/components/rest/rest-hover-aciton/rest-hover-aciton.vue';
  import type { UploadResponse } from '@/api/upload';
  import { uploadQiNiu } from './qiniu-util';
  import { isObject } from '@/utils/verify';
  import RestVideoViewer from '../rest-video-viewer/rest-video-viewer.vue';
  import { FileUploadService } from '@/utils/upload-util';
  import { useAutoAnimate } from '@formkit/auto-animate/vue';

  type FileItem = {
    /** 唯一 ID */
    __id: number | string;
    /** 浏览器中的文件对象 */
    file?: File;
    /** 进度 */
    percentage: number;
    /** 当前 index */
    index: number;
    /** 上传状态; 0:正在上传; 1:上传成功; -1:上传失败;  */
    uploadStatus: -1 | 0 | 1;
    /** 错误原因  */
    errMsg?: string;
    /** url */
    url: string;
    /** 海报 */
    poster: string;
  };
  type CustomKeys = {
    /** 绑定上传文件链接的 key（即modelValue为对象或对象数组时绑定的上传文件链接的key） */
    bindFileUrlKey?: string;
    /** 绑定返回海报链接的 key（即modelValue为对象或对象数组时绑定的海报链接的key） */
    bindPosterUrlKey?: string;
    /** 上传成功后服务器返回链接中文件链接的 key */
    upFileUrlKey?: string;
    /** 上传成功后服务器返回海报字段的的 key */
    upPosterUrlKey?: string;
  };
  const props = defineProps({
    /** 自定义 key */
    customKeys: { type: Object as PropType<CustomKeys>, default: (): CustomKeys => ({}) },
    /** 文件夹 */
    folder: { type: String, required: true },
    /** 海报 传入true表示自动截取第一帧,作为海报,false表示不需要海报,传入一个方法则表示自定义海报地址,这个方法需要返回一个File */
    poster: { type: [Boolean], default: true },
    /** 海报的存储路径 */
    posterFolder: { type: String, default: undefined },
    /** 提示文字 */
    hint: { type: String, default: '点击上传' },
    /** 已上传的文件 */
    modelValue: { type: [Array, String, Object] as PropType<K>, default: () => [] },
    /** 宽度 */
    width: { type: Number, default: 100 },
    /** 高度 */
    height: { type: Number, default: 100 },
    /** 最多上传几个文件 */
    limit: { type: Number, default: 1 }, // 最多几张
    /** 限制可以选择的文件类型(根据后缀限制) 如：['.png', 'jpg']  */
    accept: { type: Array as PropType<`.${string}`[]>, default: () => ['.png', '.jpg'] },
    /** 加载状态 */
    loading: { type: Boolean, default: false },
    /** 选择图片完成后的回调 */
    afterSelectFile: { type: Function as PropType<(data: File[]) => File[] | Promise<File[]>>, default: undefined },
    /** 上传成功后的回调 */
    afterUpload: { type: Function as PropType<(data: UploadResponse) => Record<string, unknown> | string>, default: undefined },
    /** 限制视频最大允许上传多少 kb，默认 1024 * 140  */
    maxVideoSize: { type: Number, default: 1024 * 10000 },
    /** 限制图片最小允许上传多少 kb，默认  1  */
    minImageSize: { type: Number, default: 1 },
    /** 限制图片最大允许上传多少 kb，默认 800  */
    maxImageSize: { type: Number, default: 1024 * 10000 },
    /** 图片宽高比,宽度:高度;每项数组的第一个值是宽度,第二个值是高度 ，支持多个比比例，示例： [1,1] 或 [[1,1], [2,3]] */
    imageAspectRatio: { type: Array as PropType<[number, number][] | [number, number]>, default: undefined },
  });
  const emit = defineEmits<{
    (e: 'change', list: FileItem[]): void;
    (e: 'update:loading', state: boolean): void;
    (e: 'update:modelValue', list: K): void;
    (e: 'error', myFile: FileItem): void;
  }>();
  console.log('props.maxVideoSize', props.maxVideoSize);
  const upLoading = ref(props.loading);
  const [parent] = useAutoAnimate();
  const apppStore = useAppStore();
  const bindFileUrlKey = computed(() => props.customKeys.bindFileUrlKey || 'url');
  const bindPosterUrlKey = computed(() => props.customKeys.bindPosterUrlKey || 'frameUrl');
  const upFileUrlKey = computed(() => props.customKeys.upFileUrlKey || 'url');
  const upPosterUrlKey = computed(() => props.customKeys.upPosterUrlKey || 'frameUrl');
  const restImgViewerRef: $RefComp<'RestImgViewer'> = ref(null);
  const restVideoViewerRef: $RefComp<'RestVideoViewer'> = ref(null);
  const restSelectFileRef: $RefComp<'RestSelectFile'> = ref(null);
  const newWidth = computed(() => `${props.width}px`);
  const newHeight = computed(() => `${props.height}px`);

  /** 全部的文件（包含上传和未上传的） */
  const fileList: Ref<FileItem[]> = ref(getDefFiles(props.modelValue));
  /** 得到上传成功的文件（包含本次上传成功和之前上传成功的） */
  const uploadedList = shallowRef<Record<string, string>[] | string[]>(getUploadedList());
  /** 最终需要得到文件（根据是多选还是单选整理成需要的文件） */
  const uploadedFiles = computed(() => (props.limit > 1 ? (uploadedList.value as K) : (uploadedList.value[0] as K)));
  /** 现在还可以添加多少个 */
  const efficientNumb = computed(() => props.limit - uploadedList.value.length - fileList.value.length);

  watch(
    () => upLoading.value,
    (val) => {
      emit('update:loading', val);
    }
  );
  watch(
    () => props.modelValue,
    (val) => {
      fileList.value = getDefFiles(val);
    }
  );
  watch(
    () => uploadedFiles.value,
    (val) => {
      emit('update:modelValue', val);
    }
  );

  /**
   * 预览图片
   */
  function previewImageClick(index: number) {
    const imgUrl: string = fileList.value[index]?.url || '';
    imgUrl && restImgViewerRef.value?.show([imgUrl], index);
  }

  /**
   * 视频预览
   */
  function previewVideoClick(src: string, poster: string) {
    src && restVideoViewerRef.value?.show({ src, poster });
  }

  /**
   * 删除
   */
  function deleteClick(index: number) {
    fileList.value.splice(index, 1);
    emit('change', fileList.value);
    uploadedList.value = getUploadedList();
  }
  /**
   * 重新上传
   */
  async function reUploadClick(index: number) {
    try {
      upLoading.value = true;
      await uploadFile(fileList.value[index]!);
      upLoading.value = false;
      emit('change', fileList.value);
      uploadedList.value = getUploadedList();
    } catch (error) {
      upLoading.value = false;
      uploadedList.value = getUploadedList();
      console.log('重新上传失败====', error);
    }
  }

  /**
   * 重置
   */
  function reset() {
    fileList.value = [];
    uploadedList.value = [];
  }

  /**
   * 处理上传所有图片
   */
  async function uploadFileList(files: File[] | null) {
    try {
      if (files && files.length > 0) {
        const promiseArr: FileItem[] = [];
        for (let i = 0, len = files.length; i < len; i++) {
          if (efficientNumb.value > 0 && files[i]) {
            fileList.value.push({
              file: files[i]!,
              percentage: 0,
              uploadStatus: 0,
              index: fileList.value.length,
              __id: apppStore.getBaseID(),
              url: '',
              poster: '',
            });
            promiseArr.push(fileList.value[fileList.value.length - 1]!);
          }
        }
        if (promiseArr.length > 0) {
          upLoading.value = true;
          const uploadService = new FileUploadService(uploadFile, 5);
          await uploadService.upload(promiseArr);
          upLoading.value = false;
          emit('change', fileList.value);
          uploadedList.value = getUploadedList();
        } else {
          ElToast.error('暂无可上传文件');
        }
      }
    } catch (error) {
      uploadedList.value = getUploadedList();
      ElToast.error('上传失败');
    }
  }

  /**
   * 上传文件
   */
  async function uploadFile(myFile: FileItem): Promise<FileItem> {
    try {
      const fileSize = myFile.file!.size / 1024;
      if (myFile.file!.type.includes('image')) {
        if (fileSize > props.maxImageSize) {
          const msg = getRestrictSizeMsg(props.maxImageSize, 'max');
          myFile.uploadStatus = -1;
          myFile.errMsg = msg;
          return Promise.reject({ code: -1001, msg });
        } else if (fileSize < props.minImageSize) {
          const msg = getRestrictSizeMsg(props.minImageSize, 'min');
          myFile.uploadStatus = -1;
          myFile.errMsg = msg;
          return Promise.reject({ code: -1001, msg });
        } else if (Array.isArray(props.imageAspectRatio) && props.imageAspectRatio.length >= 1) {
          const { width, height } = await getImageWidthHeight(myFile.file!);
          if (typeof props.imageAspectRatio[0] === 'number' && typeof props.imageAspectRatio[1] === 'number') {
            // 单个比例
            if (Math.floor((width / height) * 10000) !== Math.floor((props.imageAspectRatio[0] / props.imageAspectRatio[1]) * 10000)) {
              myFile.uploadStatus = -1;
              myFile.errMsg = `宽高比必须为 ${props.imageAspectRatio[0]}:${props.imageAspectRatio[1]}`;
              return Promise.reject({ code: -1001, msg: myFile.errMsg });
            }
          } else {
            // 多个比例
            const isRatio = props.imageAspectRatio.find((ratio) => {
              if (Array.isArray(ratio) && typeof ratio[0] === 'number' && typeof ratio[1] === 'number') {
                if (Math.floor((width / height) * 10000) === Math.floor((ratio[0] / ratio[1]) * 10000)) {
                  return true;
                }
              }
              return false;
            });
            if (!isRatio) {
              myFile.uploadStatus = -1;
              let errMsg = `宽高比必须为 `;

              (props.imageAspectRatio as [number, number][]).forEach((ratio) => {
                errMsg += `${ratio[0]}:${ratio[1]}, `;
              });
              myFile.errMsg = errMsg.slice(0, -2);
              return Promise.reject({ code: -1001, msg: myFile.errMsg });
            }
          }
        }
      }

      if (myFile.file!.type.includes('video') && fileSize > props.maxVideoSize) {
        const msg = getRestrictSizeMsg(props.maxVideoSize, 'min');
        myFile.uploadStatus = -1;
        myFile.errMsg = msg;
        return Promise.reject({ code: -1001, msg });
      }
      const promiseArr = [
        uploadQiNiu({
          file: myFile.file!,
          folder: props.folder,
          onUploadProgress(res) {
            myFile.percentage = Math.floor((res.percent || 0) * 10000) / 100;
          },
        }),
      ];
      // 如果是视频择需要获取并上传地一帧图片

      if (myFile.file!.type.includes('video') && props.poster) {
        promiseArr.push(uploadPosterImg(myFile));
      }

      myFile.uploadStatus = 0;
      const [res1, res2] = await Promise.all(promiseArr);
      if (res1 && res2) {
        res1.frameUrl = res2.url;
      }

      const data = typeof props.afterUpload === 'function' ? props.afterUpload(res1!) : res1!;
      if (isObject(data)) {
        const newData = data as Record<string, string>;
        myFile.url = upFileUrlKey.value in newData ? String(newData[upFileUrlKey.value] || '') : '';
        myFile.poster = upPosterUrlKey.value in newData ? String(newData[upPosterUrlKey.value] || '') : '';
      } else {
        myFile.url = String(data || '');
      }
      myFile.uploadStatus = 1;
      return Promise.resolve(myFile);
    } catch (error) {
      console.log('上传失败====', error);
      myFile.uploadStatus = -1;
      myFile.errMsg = typeof error.msg === 'string' ? error.msg : '上传失败';
      emit('error', error);
      return Promise.reject(myFile);
    }
  }

  /** 得到初始已上传的文件 */
  function getDefFiles(data: unknown): FileItem[] {
    const val: unknown[] = Array.isArray(data) ? data : data ? [data] : [];
    const newVal: FileItem[] = val.map((item, index: number) => {
      const _item: FileItem = isObject(item)
        ? {
            url: String(item[bindFileUrlKey.value] || ''),
            poster: String(item[bindPosterUrlKey.value] || ''),
            uploadStatus: 1,
            index,
            __id: apppStore.getBaseID(),
            percentage: 0,
          }
        : { url: String(item), poster: '', uploadStatus: 1, index, __id: apppStore.getBaseID(), percentage: 0 };
      return _item;
    });
    return newVal;
  }

  /**
   * 上传海报
   */
  async function uploadPosterImg(myFile: FileItem): Promise<{ url: string }> {
    try {
      let fileSize = 0;
      let poster: File | undefined = undefined;
      const videoInfo = await getFirstFrameOfVideo(myFile.file!);
      fileSize = videoInfo.firstFrame.size / 1024;
      poster = videoInfo.firstFrame;
      if (fileSize > props.maxImageSize) {
        return Promise.reject({ code: -1001, msg: getRestrictSizeMsg(props.maxImageSize, 'max') });
      } else {
        const res = await uploadQiNiu({ file: poster!, folder: props.posterFolder || props.folder });
        return Promise.resolve(res);
      }
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /** 得到已上传成功的文件 */
  function getUploadedList() {
    const list = fileList.value.filter((item) => item.uploadStatus === 1 && item.url);
    // 不能用 bindFileUrlKey 代替
    if (props.customKeys.bindFileUrlKey) {
      return list.map((item) => {
        const obj: Record<string, string> = {};
        obj[bindFileUrlKey.value] = item.url;
        obj[bindPosterUrlKey.value] = item.poster;
        return obj;
      });
    } else {
      return list.map((item) => item.url);
    }
  }

  /** 得到不符合大小限制的提示语 */
  function getRestrictSizeMsg(maxSize: number, type: 'max' | 'min') {
    const maxSizeText = maxSize >= 1024 ? `${Number((maxSize / 1024).toFixed(2))}M` : `${maxSize}KB`;
    return type === 'max' ? `文件大小不能超过${maxSizeText}` : `文件大小不能小于${maxSizeText}`;
  }

  defineExpose({
    reset,
    getLoadState: () => upLoading.value,

    /** 得到所有的 url,(包含本次新增或编辑的，以及之前上传的) */
    getAllFileUrls: () => {
      return uploadedFiles.value;
    },
  });
</script>

<style lang="scss" scoped>
  .img-upload-vessel {
    overflow: hidden;

    .complete-list {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: space-between;
      margin-top: -15px;
      margin-left: -15px;

      .drag-vessel {
        position: relative;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;
        user-select: none;
        background: var(--el-fill-color-lighter);
        border: 1px dashed var(--el-border-color-darker);
        border-radius: 6px;

        &:hover {
          border: 1px dashed var(--el-color-primary);
        }

        &.drag {
          background: var(--el-color-primary-light-9);
          border: 1px dashed var(--el-color-primary);
        }

        .logo-box {
          @include flex(column, nowrap, center, center, center);
          width: 100%;
          height: 100%;

          .logo {
            width: 40px;
            height: 40px;
            font-size: 30px;
            color: $c-text-disabled;
          }

          .text {
            font-size: 12px;
            line-height: 12px;
            color: $c-text-disabled;
          }
        }

        .file-input {
          position: absolute;
          top: 0;
          left: 0;
          z-index: 1;
          width: 100%;
          height: 100%;
          cursor: pointer;
          opacity: 0;
        }
      }

      .complete-item {
        width: var(--file-width);
        margin-top: 15px;
        margin-left: 15px;
        transition: all 0.3s;
      }
    }
  }

  html.dark .drag-vessel .logo-box .logo {
    opacity: 0.6;
  }

  .fade-move,
  .fade-enter-active,
  .fade-leave-active {
    transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
  }

  .fade-enter-from,
  .fade-leave-to {
    opacity: 0;
    transform: scaleY(0.01) translate(30px, 0);
  }

  .fade-leave-active {
    position: absolute;
  }
</style>
